package com.lam.code.configuration.shiro;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import com.lam.code.base.domain.ReturnT;
import com.lam.code.configuration.prop.JWTProperties;
import com.lam.code.entity.User;
import com.lam.code.service.UserService;
import com.lam.code.utils.HttpServletResponseUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;

/**
 * @author lam
 * @description JWT鉴权登录拦截器
 * @date 2019/12/23 22:23
 */
@Slf4j
@SuppressWarnings("all")
public class JWTFilter extends AuthenticatingFilter {
    private static final int tokenRefreshInterval = 300; //刷新token时间

    private JWTProperties jwtProperties;

    private UserService userService;

    public JWTFilter(UserService userService,JWTProperties jwtProperties){
        this.jwtProperties = jwtProperties;
        this.userService = userService;
    }

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) //对于OPTION请求做拦截，不做token校验
        {
            return false;
        }

        return super.preHandle(request, response);
    }

    @Override
    protected void postHandle(ServletRequest request, ServletResponse response){
        this.fillCorsHeader(WebUtils.toHttp(request), WebUtils.toHttp(response));
        //postHandle方法在isAccessAllowed之后执行，防止同一个请求重复被当前过滤器拦截并处理，
        // 详细可见org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal }
        request.setAttribute("JWTFilter.FILTERED", true);
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if(this.isLoginRequest(request, response)) {
            return true;
        }
        Boolean afterFiltered = (Boolean)(request.getAttribute("JWTFilter.FILTERED"));
        //若当前请求已被当前JWT过滤器拦截并处理，则直接返回true，不再执行登录
        if( BooleanUtil.isTrue(afterFiltered)) {
            return true;
        }

        boolean allowed = false;
        try {
            allowed = executeLogin(request, response);
        } catch(IllegalStateException e){ //not found any token
            log.error("Not found any token");
        }catch (Exception e) {
            log.error("Error occurs when login", e);
        }
        return allowed || super.isPermissive(mappedValue);
    }

    /**
     * 这里重写了父类的方法，使用我们自己定义的Token类，提交给shiro。这个方法返回null的话会直接抛出异常()，进入isAccessAllowed（）的异常处理逻辑
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) {
        String jwtToken = getAuthzHeader(servletRequest);
        if(StrUtil.isNotBlank(jwtToken)&&!JWTUtil.isTokenExpired(jwtToken)) {
            return new JWTToken(jwtToken);
        }

        return null;
    }

    /**
     * 重写 onAccessDenied 方法，避免父类中调用再次executeLogin
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletResponse httpResponse = WebUtils.toHttp(servletResponse);
//        httpResponse.setCharacterEncoding("UTF-8");
//        httpResponse.setContentType("application/json;charset=UTF-8");
        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        HttpServletResponseUtil.printJSON(httpResponse, ReturnT.error("token无效"));
        fillCorsHeader(WebUtils.toHttp(servletRequest), httpResponse);
        return false;
    }

    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse httpResponse = WebUtils.toHttp(response);
        String newToken = null;
        if(token instanceof JWTToken){
            JWTToken jwtToken = (JWTToken)token;
            User user = (User) subject.getPrincipal();
            boolean shouldRefresh = shouldTokenRefresh(JWTUtil.getIssuedAt(jwtToken.getToken()));
            if(shouldRefresh) {
                newToken = JWTUtil.sign(user.getId());
            }
        }
        if(StrUtil.isNotBlank(newToken)) {
            httpResponse.setHeader("x-auth-token", newToken);
        }

        return true;
    }

    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        log.error("Validate token fail, token:{}, error:{}", token.toString(), e.getMessage());
        return false;
    }

    protected String getAuthzHeader(ServletRequest request) {
        HttpServletRequest httpRequest = WebUtils.toHttp(request);
        String header = httpRequest.getHeader("x-auth-token");
        return StrUtil.removePrefix(header, "Bearer ");
    }

    protected boolean shouldTokenRefresh(Date issueAt){
        LocalDateTime issueTime = LocalDateTime.ofInstant(issueAt.toInstant(), ZoneId.systemDefault());
        return LocalDateTime.now().minusSeconds(tokenRefreshInterval).isAfter(issueTime);
    }

    protected void fillCorsHeader(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse){
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
    }

}
